package tp.badugi.server;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

/**
 * The purpose of this class is to determine the winner/winners of a badugi game.
 * In order to determine the winner/winners an instance of this class should be created
 * with the appropriate arguments passed to the constructor of the class.
 * The methods {@link #getWinners()} and {@link #getPotDivision()} provide access
 * to the determination.
 * 
 * @author bartek
 *
 */
public class BadugiWinnerResolver {
	
	private String[] rankOrder = new String[] {
			Card.ACE, Card.TWO, Card.THREE, Card.FOUR, Card.FIVE, Card.SIX, Card.SEVEN,
			Card.EIGHT, Card.NINE, Card.TEN, Card.JACK, Card.QUEEN, Card.KING
	};
	
	private List<Player> winners;
	private Map<Player, Long> potDivision;

	public BadugiWinnerResolver() {
		
	}
	
	/**
	 * Creates a <code>BadugiWinnerResolver</code> that determines the winners
	 * and how to divide the pot.
	 * 
	 * @param remainingPlayers players who remain in the game (haven't folded).
	 * @param potSize the amount of chips in the pot.
	 */
	public BadugiWinnerResolver(
			List<Player> remainingPlayers,
			long potSize) 
	{
		// Create a map that will hold each player's determined hand
		Map<Player, List<Card>> playersHands = new HashMap<Player, List<Card>>();
		
		// Determine each player's hand
		for (Player player : remainingPlayers) {
			List<Card> hand = determineHand(player.getCards());
			playersHands.put(player, hand);
		}
		
		// Determine winners
		winners = new ArrayList<Player>();
		for (Player player : remainingPlayers) {
			if (winners.size() == 0) {
				winners.add(player);
			}
			else {
				Player potentialWinner = winners.get(0);
				List<Card> potentialWinnersHand = playersHands.get(potentialWinner);
				List<Card> playersHand = playersHands.get(player);
				int compare = compareHands(playersHand, potentialWinnersHand);
				if (compare > 0) {
					winners.removeAll(remainingPlayers);
					winners.add(player);
				}
				else if (compare == 0) {
					winners.add(player);
				}
			}
		}
		int numberOfWinners = winners.size();
		
		// Sort winners by pot contributions
		boolean sorted = false;
		while (!sorted) {
			sorted = true;
			for (int i = 0; i + 1 < numberOfWinners; i++) {
				Player winner1 = winners.get(i);
				Player winner2 = winners.get(i + 1);
				if (winner1.getPotContribution() > winner2.getPotContribution()) {
					winners.set(i, winner2);
					winners.set(i + 1, winner1);
					sorted = false;
				}
			}
		}
			
		// Calculate amount of chips contributed to the pot by players who folded
		Map<Player, Long> potContribution = new HashMap<Player, Long>();
		long foldedPlayersContribution = potSize;
		for (Player player: remainingPlayers) {
			potContribution.put(player, player.getPotContribution());
			foldedPlayersContribution -= player.getPotContribution();
		}
		
		potDivision = new HashMap<Player, Long>();
		long quotient;
		long remainder;
		
		// Divide chips contributed to the pot by players who folded
		quotient = foldedPlayersContribution / numberOfWinners;
		remainder = foldedPlayersContribution % numberOfWinners;		
		for (int i = 0; i < numberOfWinners; i++) {
			Player winner = winners.get(i);
			potDivision.put(winner, i == 0 ? quotient + remainder : quotient);
		}
		
		// Divide chips contributed by players remaining in game
		for (int i = 0; i < numberOfWinners; i++) {
			Player winner = winners.get(i);
			long winnersContribution = potContribution.get(winner);
			long potCoverage = 0;
			for (Player player : remainingPlayers) {
				long playersContribution = potContribution.get(player);
				long amountToAdd = playersContribution > winnersContribution 
						? winnersContribution : playersContribution;
				potCoverage += amountToAdd;
				playersContribution -= amountToAdd;
				potContribution.put(player, playersContribution);
			}
			quotient = potCoverage / (numberOfWinners - i);
			remainder = potCoverage % (numberOfWinners - i);
			for (int j = i; j < numberOfWinners; j++) {
				Player player = winners.get(j);
				long amountWon = potDivision.get(player);
				amountWon += j == 0 ? quotient + remainder : quotient;
				potDivision.put(player, amountWon);
			}
		}
		
		// Return remaining chips to their owners
		for (Player player : remainingPlayers) {
			long amountWon = potDivision.get(player) != null ? potDivision.get(player) : 0;
			amountWon += potContribution.get(player);
			potDivision.put(player, amountWon);
		}
	}

	/**
	 * Compares two hands.
	 * 
	 * @param firstHand hand one.
	 * @param secondHand hand two.
	 * @return a negative integer, zero, or a positive integer 
	 * as the first hand is lower ranking than, equal to, or higher ranking than the second.
	 */
	public int compareHands(List<Card> firstHand, List<Card> secondHand) {
		
		// If the first hand is larger then it wins right away 
		if (firstHand.size() > secondHand.size()) {
			return 1;
		}
		
		// If the second hand is larger then it wins right away
		else if (secondHand.size() > firstHand.size()) {
			return -1;
		}
		
		// Otherwise we need to compare hands card by card
		else {
			for (int i = firstHand.size() - 1; i >= 0; i--) {
				Card cardFromFirstHand = firstHand.get(i);
				Card cardFromSecondHand = secondHand.get(i);
				int comparisonResult = compareRanks(cardFromFirstHand, cardFromSecondHand);
				
				// If card from first hand is lower ranking then first hand wins
				if (comparisonResult < 0) {
					return 1;
				}
				
				// If card from first hand is higher ranking then first hand loses
				else if (comparisonResult > 0) {
					return -1;
				}
			}
		}
		
		// If all cards match then hands are equal
		return 0;
	}
	
	public List<Card> determineHand(List<Card> playersCards) {
		List<Card> hand = new ArrayList<Card>(playersCards);
		
		// Sort player's hand from highest ranking cards to lowest (reverse order)
		Collections.sort(hand, Collections.reverseOrder());
		
		// Loop over hand while there are two cards to compare and remove duplicate ranks
		// This loop works because cards are sorted in reverse order
		for (int index = 0; index + 1 < hand.size(); ) {
			Card cardOne = hand.get(index);
			Card cardTwo = hand.get(index + 1);
			
			// If the cards are of the same rank then we need to determine which to remove
			if (cardOne.sameRank(cardTwo)) {
				
				// Compare first card with the suit of the third card to see
				// if we should remove the first card
				if (index + 2 < hand.size() && cardOne.sameSuit(hand.get(index + 2))) {
					hand.remove(cardOne);
				}
				
				// If there is no third card or it has a different suit then remove second card
				else {
					hand.remove(cardTwo);
				}
			}
			
			// If the two cards were not of the same rank then increase index and continue
			else {
				index++;
			}
		}
		
		// Now that duplicate ranks are gone, loop over hand to remove duplicate suits
		for (ListIterator<Card> it = hand.listIterator(); it.hasNext(); ) {
			Card card = it.next();
			Card otherCard = null;
			for (int i = it.nextIndex(); i < hand.size(); i++) {
				otherCard = hand.get(i);
				if (card.sameSuit(otherCard))
					break;
				else
					otherCard = null;
			}
			if (otherCard != null) {
				it.remove(); // remove card
			}
		}
		
		// Sort player's hand from lowest ranking cards to highest (natural order)
		Collections.sort(hand);
		
		return hand;
	}

	/**
	 * Returns a map to the amount of chips each player should receive.
	 * 
	 * @return amount of chips each player should be rewarded.
	 */
	public Map<Player, Long> getPotDivision() {
		return potDivision;
	}

	/**
	 * Returns the winners.
	 * 
	 * @return winners
	 */
	public List<Player> getWinners() {
		return winners;
	}
	
	/**
	 * Compares the ranks of two cards.
	 * 
	 * @param firstCard card one.
	 * @param secondCard card two.
	 * @return a negative integer, zero, or a positive integer 
	 * as the first card is lower ranking than, equal to, or higher ranking than the second.
	 */
	private int compareRanks(Card firstCard, Card secondCard) {
		String rankOfFirst = firstCard.getRank();
		String rankOfSecond = secondCard.getRank();
		
		// Both cards are equal ranking
		if (rankOfFirst.equals(rankOfSecond)) {
			return 0;
		}

		for (int i = 0; i < rankOrder.length; i++) {
			
			// First card is lower ranking
			if (rankOfFirst.equals(rankOrder[i])) {
				return -1;
			}
			
			// First card is higher ranking
			if (rankOfSecond.equals(rankOrder[i])) {
				return 1;
			}
		}
		return 0;
	}
}
